<?php

namespace Wgx\PdoEx\Helper;

use Wgx\PdoEx\PdoManager;
use Wgx\PdoEx\QueryBuilder;
use Wgx\PdoEx\QueryBuilderResult;

/**
 * 这个类只是 QueryBuilder 的一个辅助类 , 这个类提供的功能 QueryBuilder 都可以做到 .
 * QueryBuilder 为了功能清晰和方便维护 , 不会为了特定场景(例如 getById) 去封装一个特定的函数 .
 * 然后在实际开发中 , 这些针对特定场景返回特定格式结果的功能函数却是很实用的 , 很方便的 .
 * 但是如果把这些特定的功能函数放到 QueryBuilder , 很显然就会让 QueryBuilder 变得混乱 , 主次难以分辨 .
 * 所以 , 就诞生了这个类 , 我们基于 QueryBuilder 封装一些特定的功能函数放到这类中 且 随着需要随时扩展 .
 * 另外 , 这个类被设计成可以被子类继承 , 在子类中还可以继续封装一些更加特定的功能函数 .
 *
 * 注意:
 * 如果系统使用了 orm , 请优先选择使用 orm 中相应功能的函数 ,
 * orm 经常会给数据库加一层缓存 , orm 中的 增删改查 函数都会维护这个缓存 ,
 * 但是 QueryBuilder 和这个类 都是直接操作数据库的 , 是 感知不到 orm 中的缓存的 ,
 * 如果系统使用了 orm , 我们仍然经常使用 QueryBuilder 和这个类 就容易漏掉维护 orm 中的缓存 而导致出现问题 .
 */
class DbRwHelper
{
    private static $name2HelperArr = array();

    private $rwConnName;
    /** @var null|QueryBuilder */
    private $qb;
    /**
     * 暴露给子类,
     * 子类在封装一些特定的功能函数时可以设置这个成员变量,
     * 以便可以通过这个成员变量的 getter 函数暴露给用户
     * @var null|QueryBuilderResult
     */
    protected $qbResult;

    /**
     * 子类不需要重写这个函数,
     * 子类调用这个函数可以返回子类的实例,
     * 父类调用这个函数可以返回父类的实例
     * @param null|string $rwConnName - 主库连接名, 不传使用默认连接名
     * @return null|static
     */
    public static function useDb($rwConnName = null)
    {
        if (empty($rwConnName)) {
            $rwConnName = PdoManager::getSingleObj()->getDefaultName();
            if (empty($rwConnName)) {
                return null;
            }
        }

        $class = static::class;
        $key = "{$rwConnName}:{$class}";
        if (empty(self::$name2HelperArr[$key])) {
            self::$name2HelperArr[$key] = new $class($rwConnName);
        }

        return self::$name2HelperArr[$key];
    }

    /**
     * @param null|string $rwConnName - 主库连接名, 不传使用默认连接名
     */
    protected function __construct($rwConnName = null)
    {
        $this->rwConnName = $rwConnName;
    }

    /**
     * @return null|string - 主库连接名
     */
    public function getRwConnName()
    {
        return $this->rwConnName;
    }

    /**
     * @return null|QueryBuilder
     */
    public function getQb()
    {
        if (empty($this->qb)) {
            $this->qb = QueryBuilder::useDb($this->rwConnName);
        }
        return $this->qb;
    }

    /**
     * return $this->qbResult;
     * @return null|QueryBuilderResult
     */
    public function getQbResult()
    {
        return $this->qbResult;
    }

    /**
     * return empty($this->qbResult) ? '' : $this->qbResult->getSql();
     * @return string
     */
    public function getSql()
    {
        return empty($this->qbResult) ? '' : $this->qbResult->getSql();
    }

    /**
     * 使用 qb->select($this->getQb()->select) 来执行 SELECT,
     * 所以参数请参见 QueryBuilder::select 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::select 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRPdo()->quote
     *
     * @param string $table
     * @param string|string[] $columns
     * @param null|array $where
     * @param null|string|string[] $groupBy
     * @param null|array $orderBy
     * @param int $limit
     * @param int $offset
     * @return null|false|array
     */
    public function getAll($table, $columns, $where = null, $groupBy = null, $orderBy = null, $limit = 0, $offset = 0)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->select($table, $columns, $where, $groupBy, $orderBy, $limit, $offset);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return null;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        return $qbResult->getPdoStatement()->fetchAll(\PDO::FETCH_ASSOC);
    }

    /**
     * 使用 qb->select($this->getQb()->select) 来执行 SELECT,
     * 所以参数请参见 QueryBuilder::select 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::select 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRPdo()->quote
     *
     * @param string $table
     * @param string|string[] $columns
     * @param null|array $where
     * @param null|string|string[] $groupBy
     * @param null|array $orderBy
     * @return null|false|array
     */
    public function getFirstRow($table, $columns, $where = null, $groupBy = null, $orderBy = null)
    {
        $result = $this->getAll($table, $columns, $where, $groupBy, $orderBy, 1);
        return empty($result) ? $result : $result[0];
    }

    /**
     * 使用 qb->select($this->getQb()->select) 来执行 SELECT,
     * 所以参数请参见 QueryBuilder::select 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::select 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRPdo()->quote
     *
     * @param string $table
     * @param string $column - 要获取的字段名, 'col1'
     * @param null|array $where
     * @param null|string|string[] $groupBy
     * @param null|array $orderBy
     * @return null|false|mixed
     */
    public function getFirstRowFirstColumn($table, $column, $where = null, $groupBy = null, $orderBy = null)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->select($table, $column, $where, $groupBy, $orderBy, 1);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return null;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        $result = $qbResult->getPdoStatement()->fetchAll(\PDO::FETCH_NUM);
        return empty($result[0]) ? null : $result[0][0];
    }

    /**
     * 使用 qb->select($this->getQb()->select) 来执行 SELECT
     *
     * @param string $table - 表名
     * @param string|string[] $columns - 参见 QueryBuilder::select 中的 $columns 参数
     * @param int $id - 主键id, 要求表的主键字段名为 id
     * @return null|false|array
     */
    public function getById($table, $columns, $id)
    {
        $this->qbResult = null;

        $id = empty($id) ? 0 : intval($id);
        if ($id < 1) {
            return null;
        }

        return $this->getFirstRow($table, $columns, array('`id`' => $id));
    }

    /**
     * 使用 qb->select($this->getQb()->select) 来执行 SELECT
     *
     * 当 $ids 参数传入的数量太大时, 实现会将其切成一段段的来执行 SELECT, 所以不用担心传入的 $ids 数量太大导致 SELECT 太慢 .
     * 由于实现会将其切成一段段的来执行 SELECT, 所以也无法整体指定排序, 如果确实需要排序, 可以在程序中对结果进行排序
     *
     * @param string $table - 表名
     * @param string|string[] $columns - 参见 QueryBuilder::select 中的 $columns 参数
     * @param int[] $ids - 主键id的数组, 要求表的主键字段名为 id
     * @return null|array
     */
    public function getByIds($table, $columns, $ids)
    {
        $this->qbResult = null;

        if (empty($ids)) {
            return null;
        }

        $validIds = array();
        foreach ($ids as $id) {
            $id = empty($id) ? 0 : intval($id);
            if ($id > 0) {
                $validIds[$id] = 1;
            }
        }
        if (empty($validIds)) {
            return null;
        }
        $validIds = array_keys($validIds);

        $idsSize = count($validIds);
        $chunkSizeConst = 100;
        if ($idsSize == 1) {
            return $this->getAll($table, $columns, array('`id`' => $validIds[0]), null, null, 1);

        } elseif ($idsSize <= $chunkSizeConst) {
            return $this->getAll($table, $columns, array('`id`' => array('in' => $validIds)), null, null, $idsSize);

        } else {
            $chunks = array_chunk($validIds, $chunkSizeConst);
            $pdoStatement = null;
            $ret = array();
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->select($table, $columns, array('`id`' => array('in' => $chunk)), null, null, $chunkSize);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $isOk = $qbResult->getIsOk();
                    } else {
                        $isOk = $pdoStatement->execute($chunk);
                    }
                    if ($isOk) {
                        $result = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
                        if ($result) {
                            $ret = array_merge($ret, $result);
                        }
                    }

                } else {
                    if ($chunkSize == 1) {
                        $result = $this->getAll($table, $columns, array('`id`' => $chunk[0]), null, null, 1);
                    } else {
                        $result = $this->getAll($table, $columns, array('`id`' => array('in' => $chunk)), null, null, $chunkSize);
                    }
                    if ($result) {
                        $ret = array_merge($ret, $result);
                    }
                }
            }
            return $ret;
        }
    }

    /**
     * 使用 qb->insert($this->getQb()->insert) 来执行 INSERT,
     * 所以参数请参见 QueryBuilder::insert 中的同名参数
     *
     * @param string $table
     * @param array $data
     * @return false|string - 失败返回 false, 成功返回 插入数据的id
     */
    public function insert($table, $data)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->insert($table, $data);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return '';
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }

        return $qbResult->getPdo()->lastInsertId();
    }

    /**
     * 使用 qb->insertMultiValue($this->getQb()->insertMultiValue) 来执行 INSERT,
     * 所以参数请参见 QueryBuilder::insertMultiValue 中的同名参数
     *
     * 当 $data 参数传入的数量太大时, 实现会将其切成一段段的来执行 INSERT, 所以不用担心传入的 $data 数量太大 .
     * 在实现将 $data 切成一段段的来执行 INSERT 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     * 如果在使用过程中发现 sql 长度太大导致错误, 请不要轻易修改本函数中切割的段大小, 请先尝试改大一些当前 sql 会话允许的最大 sql 长度,
     * 因为本函数中切割的段大小已经调整为适合大多数场景, 所以就不要轻易的调来调去, 除非确实有充分和必要的理由
     *
     * @param string $table
     * @param string[] $columns
     * @param array $data
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertMultiValue($table, $columns, $data)
    {
        $this->qbResult = null;

        if (empty($table) || empty($columns) || empty($data)) {
            return true;
        }
        if (!is_array($columns) || !is_array($data)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $columnsCnt = count($columns);
        foreach ($data as $line) {
            if (empty($line) || !is_array($line) || count($line) < $columnsCnt) {
                throw new \InvalidArgumentException('参数传递错误');
            }
        }

        $columns = array_map(function ($column) {
            return trim(strval($column));
        }, $columns);

        $dataSize = count($data);
        $chunkSizeConst = 1000;
        if ($dataSize <= $chunkSizeConst) {
            if ($dataSize == 1) {
                $line = array_values($data[0]);
                $dataMap = array();
                for ($i = 0; $i < $columnsCnt; $i++) {
                    $dataMap[$columns[$i]] = $line[$i];
                }
                $qbResult = $this->getQb()->insert($table, $dataMap);
            } else {
                $qbResult = $this->getQb()->insertMultiValue($table, $columns, $data);
            }
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return true;
            }
            return $qbResult->getIsOk();

        } else {
            $chunks = array_chunk($data, $chunkSizeConst);
            $pdoStatement = null;
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->insertMultiValue($table, $columns, $chunk);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $isOk = $qbResult->getIsOk();
                    } else {
                        $sqlParams = array();
                        foreach ($chunk as $line) {
                            $line = array_values($line);
                            for ($i = 0; $i < $columnsCnt; $i++) {
                                $sqlParams[] = $line[$i];
                            }
                        }
                        $isOk = $pdoStatement->execute($sqlParams);
                    }
                    if (empty($isOk)) {
                        return false;
                    }

                } else {
                    if ($chunkSize == 1) {
                        $line = array_values($chunk[0]);
                        $dataMap = array();
                        for ($i = 0; $i < $columnsCnt; $i++) {
                            $dataMap[$columns[$i]] = $line[$i];
                        }
                        $qbResult = $this->getQb()->insert($table, $dataMap);
                    } else {
                        $qbResult = $this->getQb()->insertMultiValue($table, $columns, $chunk);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    if (!$qbResult->getIsOk()) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    /**
     * 重载 insertMultiValue,
     * 由于 php 没有重载机制, 所以换了一个函数名, 其目的和 insertMultiValue 相同, 只是参数不同,
     *
     * 使用 qb->insertMultiValue($this->getQb()->insertMultiValue) 来执行 INSERT
     *
     * 当 $values 参数传入的数量太大时, 实现会将其切成一段段的来执行 INSERT, 所以不用担心传入的 $values 数量太大 .
     * 在实现将 $values 切成一段段的来执行 INSERT 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     * 如果在使用过程中发现 sql 长度太大导致错误, 请不要轻易修改本函数中切割的段大小, 请先尝试改大一些当前 sql 会话允许的最大 sql 长度,
     * 因为本函数中切割的段大小已经调整为适合大多数场景, 所以就不要轻易的调来调去, 除非确实有充分和必要的理由
     *
     * @param string $table - 参见 QueryBuilder::insertMultiValue 中的 $table 参数
     * @param array $values - object array, [[column1=>val11, column2=>val12, ...], [column1=>val21, column2=>val22, ...], ...]
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertValues($table, $values)
    {
        $this->qbResult = null;

        if (empty($table) || empty($values)) {
            return true;
        }
        if (!is_array($values) || empty($values[0]) || !is_array($values[0])) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        if (count($values) == 1) {
            $qbResult = $this->getQb()->insert($table, $values[0]);
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return true;
            }
            return $qbResult->getIsOk();

        } else {
            $columns = array_keys($values[0]);
            if (empty($columns)) {
                throw new \InvalidArgumentException('参数传递错误');
            }
            $data = array();
            foreach ($values as $value) {
                $newVal = array();
                foreach ($columns as $column) {
                    if (!isset($value[$column])) {
                        throw new \InvalidArgumentException('参数传递错误');
                    }
                    $newVal[] = $value[$column];
                }
                $data[] = $newVal;
            }
            return $this->insertMultiValue($table, $columns, $data);
        }
    }

    /**
     * 使用 qb->insertOrUpdate($this->getQb()->insertOrUpdate) 来执行 INSERT,
     * 所以参数请参见 QueryBuilder::insertOrUpdate 中的同名参数
     *
     * @param string $table
     * @param array $data
     * @param string[] $uniqueKeys
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertOrUpdate($table, $data, $uniqueKeys)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->insertOrUpdate($table, $data, $uniqueKeys);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return true;
        }
        return $qbResult->getIsOk();
    }

    /**
     * 使用 qb->insertMultiValueOrUpdate($this->getQb()->insertMultiValueOrUpdate) 来执行 INSERT,
     * 所以参数请参见 QueryBuilder::insertMultiValueOrUpdate 中的同名参数
     *
     * 当 $data 参数传入的数量太大时, 实现会将其切成一段段的来执行 INSERT, 所以不用担心传入的 $data 数量太大 .
     * 在实现将 $data 切成一段段的来执行 INSERT 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     * 如果在使用过程中发现 sql 长度太大导致错误, 请不要轻易修改本函数中切割的段大小, 请先尝试改大一些当前 sql 会话允许的最大 sql 长度,
     * 因为本函数中切割的段大小已经调整为适合大多数场景, 所以就不要轻易的调来调去, 除非确实有充分和必要的理由
     *
     * @param string $table
     * @param string[] $columns
     * @param string[] $uniqueKeys
     * @param array $data
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertMultiValueOrUpdate($table, $columns, $uniqueKeys, $data)
    {
        $this->qbResult = null;

        if (empty($table) || empty($columns) || empty($uniqueKeys) || empty($data)) {
            return true;
        }
        if (!is_array($columns) || !is_array($uniqueKeys) || !is_array($data)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $columnsCnt = count($columns);
        foreach ($data as $line) {
            if (empty($line) || !is_array($line) || count($line) < $columnsCnt) {
                throw new \InvalidArgumentException('参数传递错误');
            }
        }

        $columns = array_map(function ($column) {
            return trim(strval($column));
        }, $columns);

        $dataSize = count($data);
        $chunkSizeConst = 1000;
        if ($dataSize <= $chunkSizeConst) {
            if ($dataSize == 1) {
                $line = array_values($data[0]);
                $dataMap = array();
                for ($i = 0; $i < $columnsCnt; $i++) {
                    $dataMap[$columns[$i]] = $line[$i];
                }
                $qbResult = $this->getQb()->insertOrUpdate($table, $dataMap, $uniqueKeys);
            } else {
                $qbResult = $this->getQb()->insertMultiValueOrUpdate($table, $columns, $uniqueKeys, $data);
            }
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return true;
            }
            return $qbResult->getIsOk();

        } else {
            $chunks = array_chunk($data, $chunkSizeConst);
            $pdoStatement = null;
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->insertMultiValueOrUpdate($table, $columns, $uniqueKeys, $chunk);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $isOk = $qbResult->getIsOk();
                    } else {
                        $sqlParams = array();
                        foreach ($chunk as $line) {
                            $line = array_values($line);
                            for ($i = 0; $i < $columnsCnt; $i++) {
                                $sqlParams[] = $line[$i];
                            }
                        }
                        $isOk = $pdoStatement->execute($sqlParams);
                    }
                    if (empty($isOk)) {
                        return false;
                    }

                } else {
                    if ($chunkSize == 1) {
                        $line = array_values($chunk[0]);
                        $dataMap = array();
                        for ($i = 0; $i < $columnsCnt; $i++) {
                            $dataMap[$columns[$i]] = $line[$i];
                        }
                        $qbResult = $this->getQb()->insertOrUpdate($table, $dataMap, $uniqueKeys);
                    } else {
                        $qbResult = $this->getQb()->insertMultiValueOrUpdate($table, $columns, $uniqueKeys, $chunk);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    if (!$qbResult->getIsOk()) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    /**
     * 重载 insertMultiValueOrUpdate,
     * 由于 php 没有重载机制, 所以换了一个函数名, 其目的和 insertMultiValueOrUpdate 相同, 只是参数不同,
     *
     * 使用 qb->insertMultiValueOrUpdate($this->getQb()->insertMultiValueOrUpdate) 来执行 INSERT
     *
     * 当 $values 参数传入的数量太大时, 实现会将其切成一段段的来执行 INSERT, 所以不用担心传入的 $values 数量太大 .
     * 在实现将 $values 切成一段段的来执行 INSERT 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     * 如果在使用过程中发现 sql 长度太大导致错误, 请不要轻易修改本函数中切割的段大小, 请先尝试改大一些当前 sql 会话允许的最大 sql 长度,
     * 因为本函数中切割的段大小已经调整为适合大多数场景, 所以就不要轻易的调来调去, 除非确实有充分和必要的理由
     *
     * @param string $table - 参见 QueryBuilder::insertMultiValueOrUpdate 中的 $table 参数
     * @param array $values - object array, [[column1=>val11, column2=>val12, ...], [column1=>val21, column2=>val22, ...], ...]
     * @param string[] $uniqueKeys - 参见 QueryBuilder::insertMultiValueOrUpdate 中的 $uniqueKeys 参数
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertValuesOrUpdate($table, $values, $uniqueKeys)
    {
        $this->qbResult = null;

        if (empty($table) || empty($values) || empty($uniqueKeys)) {
            return true;
        }
        if (!is_array($values) || empty($values[0]) || !is_array($values[0])) {
            throw new \InvalidArgumentException('参数传递错误');
        }
        if (!is_array($uniqueKeys)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        if (count($values) == 1) {
            $qbResult = $this->getQb()->insertOrUpdate($table, $values[0], $uniqueKeys);
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return true;
            }
            return $qbResult->getIsOk();

        } else {
            $columns = array_keys($values[0]);
            if (empty($columns)) {
                throw new \InvalidArgumentException('参数传递错误');
            }
            $data = array();
            foreach ($values as $value) {
                $newVal = array();
                foreach ($columns as $column) {
                    if (!isset($value[$column])) {
                        throw new \InvalidArgumentException('参数传递错误');
                    }
                    $newVal[] = $value[$column];
                }
                $data[] = $newVal;
            }
            return $this->insertMultiValueOrUpdate($table, $columns, $uniqueKeys, $data);
        }
    }

    /**
     * 使用 qb->update($this->getQb()->update) 来执行 UPDATE,
     * 所以参数请参见 QueryBuilder::update 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::update 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRwPdo()->quote
     *
     * @param string $table
     * @param array $data
     * @param null|array $where
     * @param null|array $orderBy
     * @param int $limit
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function update($table, $data, $where = null, $orderBy = null, $limit = 0)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->update($table, $data, $where, $orderBy, $limit);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return 0;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        return $qbResult->getPdoStatement()->rowCount();
    }

    /**
     * 使用 qb->updateMultiValue($this->getQb()->updateMultiValue) 来执行 UPDATE,
     * 所以参数请参见 QueryBuilder::updateMultiValue 中的同名参数
     *
     * 当 $data 参数传入的数量太大时, 实现会将其切成一段段的来执行 UPDATE, 所以不用担心传入的 $data 数量太大 .
     * 在实现将 $data 切成一段段的来执行 UPDATE 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     *
     * @param string $table
     * @param string[] $columns
     * @param array $data
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateMultiValue($table, $columns, $data)
    {
        $this->qbResult = null;

        if (empty($table) || empty($columns) || empty($data)) {
            return 0;
        }
        if (!is_array($columns) || !is_array($data)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $columnsCnt = count($columns);
        foreach ($data as $id => $line) {
            if (empty($id) || empty($line) || !is_array($line) || count($line) < $columnsCnt) {
                throw new \InvalidArgumentException('参数传递错误');
            }
        }

        $columns = array_map(function ($column) {
            return trim(strval($column));
        }, $columns);

        $dataSize = count($data);
        $chunkSizeConst = 100;
        if ($dataSize <= $chunkSizeConst) {
            if ($dataSize == 1) {
                $id = null;
                $line = null;
                foreach ($data as $id => $line) {
                    break;
                }
                $line = array_values($line);

                $dataMap = array();
                for ($i = 0; $i < $columnsCnt; $i++) {
                    $dataMap[$columns[$i]] = $line[$i];
                }
                $qbResult = $this->getQb()->update($table, $dataMap, array('`id`' => $id), null, 1);
            } else {
                $qbResult = $this->getQb()->updateMultiValue($table, $columns, $data);
            }
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return 0;
            }
            if (!$qbResult->getIsOk()) {
                return false;
            }
            return $qbResult->getPdoStatement()->rowCount();

        } else {
            $chunks = array_chunk($data, $chunkSizeConst, true);
            $pdoStatement = null;
            $upCntSum = 0;
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->updateMultiValue($table, $columns, $chunk);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $isOk = $qbResult->getIsOk();
                    } else {
                        $sqlParams = array();
                        foreach ($chunk as $id => $line) {
                            $sqlParams[] = $id;
                            $line = array_values($line);
                            for ($i = 0; $i < $columnsCnt; $i++) {
                                $sqlParams[] = $line[$i];
                            }
                        }
                        $isOk = $pdoStatement->execute($sqlParams);
                    }
                    if (empty($isOk)) {
                        return false;
                    }
                    $upCntSum += intval($pdoStatement->rowCount());

                } else {
                    if ($chunkSize == 1) {
                        $id = null;
                        $line = null;
                        foreach ($chunk as $id => $line) {
                            break;
                        }
                        $line = array_values($line);

                        $dataMap = array();
                        for ($i = 0; $i < $columnsCnt; $i++) {
                            $dataMap[$columns[$i]] = $line[$i];
                        }
                        $qbResult = $this->getQb()->update($table, $dataMap, array('`id`' => $id), null, 1);
                    } else {
                        $qbResult = $this->getQb()->updateMultiValue($table, $columns, $chunk);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    if (!$qbResult->getIsOk()) {
                        return false;
                    }
                    $upCntSum += intval($qbResult->getPdoStatement()->rowCount());
                }
            }
            return $upCntSum;
        }
    }

    /**
     * 重载 updateMultiValue,
     * 由于 php 没有重载机制, 所以换了一个函数名, 其目的和 updateMultiValue 相同, 只是参数不同,
     *
     * 使用 qb->updateMultiValue($this->getQb()->updateMultiValue) 来执行 UPDATE
     *
     * 当 $values 参数传入的数量太大时, 实现会将其切成一段段的来执行 UPDATE, 所以不用担心传入的 $values 数量太大 .
     * 在实现将 $values 切成一段段的来执行 UPDATE 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     *
     * @param string $table - 参见 QueryBuilder::updateMultiValue 中的 $table 参数
     * @param array $values - map[map], [id1 => [column1=>val11, column2=>val12, ...], id2 => [column1=>val21, column2=>val22, ...], ...]
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateValues($table, $values)
    {
        $this->qbResult = null;

        if (empty($table) || empty($values)) {
            return 0;
        }
        if (!is_array($values)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $id = null;
        $value = null;
        foreach ($values as $id => $value) {
            break;
        }
        if (empty($id) || empty($value) || !is_array($value)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        if (count($values) == 1) {
            return $this->update($table, $value, array('`id`' => $id), null, 1);

        } else {
            $columns = array_keys($value);
            if (empty($columns)) {
                throw new \InvalidArgumentException('参数传递错误');
            }
            $data = array();
            foreach ($values as $id => $value) {
                $newVal = array();
                foreach ($columns as $column) {
                    if (!isset($value[$column])) {
                        throw new \InvalidArgumentException('参数传递错误');
                    }
                    $newVal[] = $value[$column];
                }
                $data[$id] = $newVal;
            }
            return $this->updateMultiValue($table, $columns, $data);
        }
    }

    /**
     * 使用 qb->update($this->getQb()->update) 来执行 UPDATE
     *
     * @param string $table - 表名
     * @param int $id - 主键id, 要求表的主键字段名为 id
     * @param array $data - 参见 QueryBuilder::update 中的 $data 参数
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateById($table, $id, $data)
    {
        $this->qbResult = null;

        $id = empty($id) ? 0 : intval($id);
        if ($id < 1) {
            return 0;
        }

        return $this->update($table, $data, array('`id`' => $id), null, 1);
    }

    /**
     * 使用 qb->update($this->getQb()->update) 来执行 UPDATE
     *
     * 当 $ids 参数传入的数量太大时, 实现会将其切成一段段的来执行 UPDATE, 所以不用担心传入的 $ids 数量太大导致 UPDATE 太慢 .
     * 在实现将 $ids 切成一段段的来执行 UPDATE 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性
     *
     * @param string $table - 表名
     * @param int[] $ids - 主键id的数组, 要求表的主键字段名为 id
     * @param array $data - 参见 QueryBuilder::update 中的 $data 参数
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateByIds($table, $ids, $data)
    {
        $this->qbResult = null;

        if (empty($ids)) {
            return 0;
        }

        $validIds = array();
        foreach ($ids as $id) {
            $id = empty($id) ? 0 : intval($id);
            if ($id > 0) {
                $validIds[$id] = 1;
            }
        }
        if (empty($validIds)) {
            return 0;
        }
        $validIds = array_keys($validIds);

        $idsSize = count($validIds);
        $chunkSizeConst = 100;
        if ($idsSize == 1) {
            return $this->update($table, $data, array('`id`' => $validIds[0]), null, 1);

        } elseif ($idsSize <= $chunkSizeConst) {
            return $this->update($table, $data, array('`id`' => array('in' => $validIds)), null, $idsSize);

        } else {
            $chunks = array_chunk($validIds, $chunkSizeConst);
            $pdoStatement = null;
            $dataParams = array();
            $upCntSum = 0;
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->update($table, $data, array('`id`' => array('in' => $chunk)), null, $chunkSize);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $sqlParams = $qbResult->getSqlParams();
                        $dataParamsLen = count($sqlParams) - $chunkSize;
                        if ($dataParamsLen > 0) {
                            $dataParams = array_splice($sqlParams, 0, $dataParamsLen);
                        }
                        $isOk = $qbResult->getIsOk();
                    } else {
                        if (empty($dataParams)) {
                            $isOk = $pdoStatement->execute($chunk);
                        } else {
                            $isOk = $pdoStatement->execute(array_merge($dataParams, $chunk));
                        }
                    }
                    if (empty($isOk)) {
                        return false;
                    }
                    $upCntSum += intval($pdoStatement->rowCount());

                } else {
                    if ($chunkSize == 1) {
                        $upCnt = $this->update($table, $data, array('`id`' => $chunk[0]), null, 1);
                    } else {
                        $upCnt = $this->update($table, $data, array('`id`' => array('in' => $chunk)), null, $chunkSize);
                    }
                    if ($upCnt === false) {
                        return false;
                    }
                    $upCntSum += intval($upCnt);
                }
            }
            return $upCntSum;
        }
    }

    /**
     * 使用 qb->delete($this->getQb()->delete) 来执行 DELETE,
     * 所以参数请参见 QueryBuilder::delete 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::delete 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRwPdo()->quote
     *
     * @param string $table
     * @param null|array $where
     * @param null|array $orderBy
     * @param int $limit
     * @return false|int - 失败返回 false, 成功返回 删除的行数
     */
    public function delete($table, $where = null, $orderBy = null, $limit = 0)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->delete($table, $where, $orderBy, $limit);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return 0;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        return $qbResult->getPdoStatement()->rowCount();
    }

    /**
     * 使用 qb->delete($this->getQb()->delete) 来执行 DELETE
     *
     * @param string $table - 表名
     * @param int $id - 主键id, 要求表的主键字段名为 id
     * @return false|int - 失败返回 false, 成功返回 删除的行数
     */
    public function deleteById($table, $id)
    {
        $this->qbResult = null;

        $id = empty($id) ? 0 : intval($id);
        if ($id < 1) {
            return 0;
        }

        return $this->delete($table, array('`id`' => $id), null, 1);
    }

    /**
     * 使用 qb->delete($this->getQb()->delete) 来执行 DELETE
     *
     * 当 $ids 参数传入的数量太大时, 实现会将其切成一段段的来执行 DELETE, 所以不用担心传入的 $ids 数量太大导致 DELETE 太慢 .
     * 在实现将 $ids 切成一段段的来执行 DELETE 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性
     *
     * @param string $table - 表名
     * @param int[] $ids - 主键id的数组, 要求表的主键字段名为 id
     * @return false|int - 失败返回 false, 成功返回 删除的行数
     */
    public function deleteByIds($table, $ids)
    {
        $this->qbResult = null;

        if (empty($ids)) {
            return 0;
        }

        $validIds = array();
        foreach ($ids as $id) {
            $id = empty($id) ? 0 : intval($id);
            if ($id > 0) {
                $validIds[$id] = 1;
            }
        }
        if (empty($validIds)) {
            return 0;
        }
        $validIds = array_keys($validIds);

        $idsSize = count($validIds);
        $chunkSizeConst = 100;
        if ($idsSize == 1) {
            return $this->delete($table, array('`id`' => $validIds[0]), null, 1);

        } elseif ($idsSize <= $chunkSizeConst) {
            return $this->delete($table, array('`id`' => array('in' => $validIds)), null, $idsSize);

        } else {
            $chunks = array_chunk($validIds, $chunkSizeConst);
            $pdoStatement = null;
            $delCntSum = 0;
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->delete($table, array('`id`' => array('in' => $chunk)), null, $chunkSize);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $isOk = $qbResult->getIsOk();
                    } else {
                        $isOk = $pdoStatement->execute($chunk);
                    }
                    if (empty($isOk)) {
                        return false;
                    }
                    $delCntSum += intval($pdoStatement->rowCount());

                } else {
                    if ($chunkSize == 1) {
                        $delCnt = $this->delete($table, array('`id`' => $chunk[0]), null, 1);
                    } else {
                        $delCnt = $this->delete($table, array('`id`' => array('in' => $chunk)), null, $chunkSize);
                    }
                    if ($delCnt === false) {
                        return false;
                    }
                    $delCntSum += intval($delCnt);
                }
            }
            return $delCntSum;
        }
    }

}